State Persistence
Note: Spartacus 2.x is no longer maintained. Please upgrade to the latest version.
Note: This feature is introduced with version 2.0 of the Spartacus libraries.
Prior to Spartacus version 2.0, the only way to achieve state persistence was by using a simple, declarative mechanism called storageSync
, which allowed you to provide properties keys to persist in the store. When you started your application, these persisted keys were used to set the initial state in the store. With Spartacus version 2.0 or newer, you can persist the state of your storefront application by using the StatePersistenceService
, and specifically, its syncWithStorage
method. It is not as simple as storageSync
, but it provides a lot more control using context and dedicated onRead
callback.
You can pass the following options to the syncWithStorage
function: key
, state$
, context$
, storageType
, and onRead
.
These options work as follows:
key
is used to distinguish one feature from another in storage. For example, to store the active cart id, you can use thecart
key, and for the user session data, you can use thesession
key.state$
is an observable that emits a value every time you want to save the new value to the persistent storage. For example, to persist the active cart id every time the active cart id changes, this observable emits a new value.context$
is an observable that describes a valid context for a particular state. For example, the active cart id is valid only for one base site. On different base sites, you want to use different carts. In this case, withcontext$
, you would use an observable that emits the base site every time it changes.storageType
specifies the storage type that is used. By default, the storage type is local storage, but you can change this to session storage, for example.onRead
is a callback that is invoked every time the context changes. To use the cart as an example, every time you change the base site, this callback is called with a value read from storage for that particular context. It will dispatch a value ofundefined
if there is nothing saved in storage.
Implementing Complete State Persistence for a Feature
The following steps describe how to implement state persistence for a feature, using the cart as an example:
-
Create a service for cart state persistence.
The following is an example:
// Import dependencies. @Injectable({ providedIn: 'root', }) export class MultiCartStatePersistenceService { constructor( protected statePersistenceService: StatePersistenceService, protected store: Store<StateWithMultiCart>, protected siteContextParamsService: SiteContextParamsService ) {} public sync() { this.statePersistenceService.syncWithStorage({ // We use this key to distinguish from other features. key: 'cart', state$: this.getCartState(), // The cart is only valid on one base site, so we use the base site as a context value. // For common cases (language/currency/base site) `SiteContextParamsService.getValues` might be useful. // If the persisted value does not depend on any context, you can skip the `context$` parameter. // For more custom solutions, you might use anything here. context$: this.siteContextParamsService.getValues([BASE_SITE_CONTEXT_ID]), // We point to our read callback, that will be called with the value restored from persisted storage. // We will restore the value on every context change. If `context$` was not given, the `onRead` callback will be invoked only once, on application start. onRead: (state) => this.onRead(state), }); } // Every time the active cart changes, we emit a new value here to trigger a save to storage. protected getCartState(): Observable<{ active: string }> { return this.store.pipe( select(MultiCartSelectors.getMultiCartState), filter((state) => !!state), // As an optimization, we try to emit only when necessary. distinctUntilKeyChanged('active'), map((state) => { return { active: state.active, }; }) ); } protected onRead(state: { active: string }) { // We always want to clear the cart state when we enter the base site. this.store.dispatch(new CartActions.ClearCartState()); // The state might be undefined, when there isn't any data in storage. // When the state is defined, its type will be the same as the inner type of the `state$` observable. if (state) { // If we get the state, we want to point to the read active cart id. this.store.dispatch(new CartActions.SetActiveCartId(state.active)); } } }
-
Provide the created service when the application is initialized.
The following is an example:
// Import dependencies. // Create factory to invoke `sync` method. export function cartStatePersistenceFactory( cartStatePersistenceService: MultiCartStatePersistenceService, configInit: ConfigInitializerService ) { const result = () => // With async configuration we want to wait for stable configuration. // We have a dependency on the base site that is part of the context config. configInit.getStableConfig('context').then(() => { // Initialize state persistence mechanism. cartStatePersistenceService.sync(); }); return result; } // It is recommended to couple state persistence with its main feature module. @NgModule({ imports: [MultiCartStoreModule, CartEventModule], }) export class CartModule { static forRoot(): ModuleWithProviders<CartModule> { return { ngModule: CartModule, providers: [ // Other providers here. { // Run factory on APP_INITIALIZER provide: APP_INITIALIZER, useFactory: cartStatePersistenceFactory, deps: [MultiCartStatePersistenceService, ConfigInitializerService], multi: true, }, ], }; } }
State Synchronization in Action
The following is an example description of state synchronization with an electronics and an apparel storefront. In this example, the state persistence service is set up as follows:
key
:"cart"
state$
: points to the active cart id selectorcontext$
:this.siteContextParamsService.getValues([BASE_SITE_CONTEXT_ID])
// The cart is valid only on the same base siteonRead
: function dispatchesClearCart
action, and thenSetActiveCartId
when the id is read from storage
The following describes an example flow of the state synchronization:
-
Access the
electronics
base site for the first time.The
onRead
callback is invoked withundefined
, because there was nothing under thespartacus⚿electronics⚿cart
key in local storage. In this implementation ofonRead
, the cart state is cleared by dispatching theClearCart
action. -
Add something to the cart.
In the background, the cart is created, and the active cart id selector emits a new value. Now the active cart id is saved under the
spartacus⚿electronics⚿cart
key in local storage. -
Switch from the
electronics
to theapparel
site, where there are a few items already added to the cart.The
onRead
callback is invoked with the active cart id for this site, which is read from thespartacus⚿apparel⚿cart
key in storage. In this example implementation, theonRead
clears the state and sets the active cart id. Then,ActiveCartService
selects this id and loads the cart that was created in a previous session. -
Return to the
electronics
site.The same steps repeat. The
onRead
is invoked with the cart id from theelectronics
site, theapparel
ngrx state is cleared, and the correct cart is loaded.